﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using NativeCode.Core.Data.Attributes;
using NativeCode.Core.Extensions;

namespace NativeCode.Core.Data
{
	public static class FilterableTypesRegistry
	{
		private static readonly Dictionary<string, QueryFilter> Filters = new Dictionary<string, QueryFilter>();
		private static readonly Dictionary<string, ParameterExpressionCache> CachedParameterExpressions = new Dictionary<string, ParameterExpressionCache>();

		#region Properties

		public static IEnumerable<QueryFilter> RegisteredFilters
		{
			get { return Filters.Values.ToList(); }
		}

		public static IEnumerable<Type> RegisteredTypes
		{
			get { return CachedParameterExpressions.Values.Select(cpe => cpe.Type).Distinct(); }
		}

		#endregion

		#region Registration

		public static void RegisterFilter(string name, QueryFilter filter)
		{
			if (!Filters.ContainsKey(name))
			{
				Filters.Add(name, filter);
			}
		}

		public static void UnregisterFilter(string name)
		{
			if (Filters.ContainsKey(name))
			{
				Filters.Remove(name);
			}
		}

		public static void ClearRegisteredFilters()
		{
			Filters.Clear();
		}

		#endregion

		public static QueryFilter GetFilterByName(string name)
		{
			return Filters[name];
		}

		public static IEnumerable<QueryFilter> GetFiltersFor(Type type)
		{
			return Filters.Values.Where(filter => filter.FilterType.Name == type.Name);
		}

		public static void AutoCreateFiltersFromAppDomain(AppDomain appDomain)
		{
			AutoCreateFiltersFromAssemblies(appDomain.GetAssemblies());
		}

		/// <remark>
		/// DO NOT FORMAT THIS INTO A LINQ QUERY.
		/// </remark>
		public static void AutoCreateFiltersFromAssemblies(IEnumerable<Assembly> assemblies)
		{
			// This is intentionally imperative for maintenance purposes. Do not turn into a query.
			foreach (Assembly assembly in assemblies)
			{
				IEnumerable<Type> filterableTypes = FindFilterableTypes(assembly);
				foreach (Type filterableType in filterableTypes)
				{
					IEnumerable<PropertyInfo> filterableProperties = FindFilterableProperties(filterableType);
					foreach (PropertyInfo filterableProperty in filterableProperties)
					{
						IEnumerable<QueryFilter> filters = BuildDefaultFilters(filterableType, filterableProperty);
						foreach (QueryFilter filter in filters)
						{
							Filters.Add(filter.Key, filter);
						}
					}
				}
			}
		}

		#region Helpers

		private static IEnumerable<Type> FindFilterableTypes(Assembly assembly)
		{
			return assembly.GetTypes().Where(type => type.GetCustomAttributes(typeof (FilterableAttribute), true).Any());
		}

		/// <summary>
		/// This will default to all properties if you do not have at least one FilterableAttribute decorated
		/// on a property. You did say you wanted the class filterable but you didn't say which properties, so
		/// now you get them all! Next time, don't ask dumb questions. We will always ignore properties that
		/// containe the NotFilterableAttribute, even if FilterAttribute is assigned to the same property.
		/// </summary>
		/// <param name="type"></param>
		/// <returns></returns>
		private static IEnumerable<PropertyInfo> FindFilterableProperties(Type type)
		{
			IEnumerable<PropertyInfo> propertiesWithAttribute = type.GetProperties()
				.Where(property => property.GetCustomAttributes(typeof (FilterableAttribute), false).Any()
				                   && !property.GetCustomAttributes(typeof (NotFilterableAttribute), false).Any());

			if (propertiesWithAttribute.Any())
			{
				return propertiesWithAttribute;
			}

			return type.GetProperties().Where(property => !property.GetCustomAttributes(typeof(NotFilterableAttribute), false).Any());
		}

		/// <summary>
		/// Goes through and find all of the possible filters we can create. We have to account for filters that
		/// return default(Expression) or where the QueryFilterOperator is None and exclude them.
		/// </summary>
		/// <param name="dataItemType"></param>
		/// <param name="propertyInfo"></param>
		/// <returns></returns>
		private static IEnumerable<QueryFilter> BuildDefaultFilters(Type dataItemType, PropertyInfo propertyInfo)
		{
			// This is intentionally using the Enum.GetNames function instead of the faster Enum.GetValues and cast.
			// This is because we need the name anyway, no point in doing it fast just to turn around and get the name.
			IEnumerable<QueryFilter> filters
				= (from filterOperatorValue in Enum.GetNames(typeof (QueryFilterOperator))
				   let queryFilterOperator = (QueryFilterOperator) Enum.Parse(typeof (QueryFilterOperator), filterOperatorValue)
				   let expression = BuildExpression(dataItemType, propertyInfo, queryFilterOperator, false)
				   let key = CreateFilterKey(dataItemType, propertyInfo, queryFilterOperator)
				   where (queryFilterOperator != QueryFilterOperator.None) && (expression != default(Expression))
				   select new QueryFilter(key, propertyInfo.Name, propertyInfo.Name, queryFilterOperator, expression, null, dataItemType)).ToList();

			return filters;
		}

		/// <summary>
		/// Iterates through all of the available QbeFilterOperators and tries to build common comparison expressions for them.
		/// </summary>
		/// <param name="dataItemType"></param>
		/// <param name="propertyInfo"></param>
		/// <param name="queryFilterOperator"></param>
		/// <param name="ignoreCase"></param>
		/// <returns></returns>
		private static Expression BuildExpression(Type dataItemType, PropertyInfo propertyInfo, QueryFilterOperator queryFilterOperator, bool ignoreCase)
		{
			try
			{
				ParameterExpression type = Expression.Parameter(dataItemType, dataItemType.Name.ToLower());
				ParameterExpression typeProperty = Expression.Parameter(propertyInfo.PropertyType, propertyInfo.Name.ToLower());

				// We have to cache the parameter expressions so that if we see them again we can use
				// the same reference. This is to ensure that the same filter can be applied multiple
				// times.
				string cacheKey = CreateCacheKey(dataItemType, typeProperty);
				if (CachedParameterExpressions.ContainsKey(cacheKey))
				{
					typeProperty = CachedParameterExpressions[cacheKey].ParameterExpression;
				}
				else
				{
					CachedParameterExpressions.Add(cacheKey, new ParameterExpressionCache
					{
						ParameterExpression = typeProperty,
						Type = dataItemType,
					});
				}

				MemberExpression propertyName = Expression.MakeMemberAccess(type, propertyInfo);

				switch (queryFilterOperator)
				{
					case QueryFilterOperator.Contains:
						if (propertyInfo.PropertyType.IsString())
						{
							MethodInfo containsMethodInfo = propertyInfo.PropertyType.GetMethods().FirstOrDefault(method => method.Name == "Contains");
							if (containsMethodInfo != null)
							{
								MethodCallExpression containsMethodExpression = Expression.Call(propertyName, containsMethodInfo, typeProperty);

								// TODO: Make this work?
								if (ignoreCase)
								{
									MethodInfo toLowerMethodInfo = propertyInfo.PropertyType.GetMethods().FirstOrDefault(method => method.Name == "ToLower");
									if (toLowerMethodInfo != null)
									{
										MethodCallExpression toLowerMethodExpression = Expression.Call(toLowerMethodInfo, typeProperty);
										Expression toLowerExpression = Expression.Lambda(toLowerMethodExpression);
										return Expression.Lambda(toLowerExpression, type, typeProperty);
									}
								}

								return Expression.Lambda(containsMethodExpression, type, typeProperty);
							}
						}
						break;
					case QueryFilterOperator.Equals:
						return Expression.Lambda(Expression.Equal(propertyName, typeProperty), type, typeProperty);
					case QueryFilterOperator.NotEquals:
						return Expression.Lambda(Expression.NotEqual(propertyName, typeProperty), type, typeProperty);
					case QueryFilterOperator.GreaterThan:
						if (propertyInfo.PropertyType.IsInequality())
						{
							return Expression.Lambda(Expression.GreaterThan(propertyName, typeProperty), type, typeProperty);
						}
						break;
					case QueryFilterOperator.GreaterThanOrEqualTo:
						if (propertyInfo.PropertyType.IsInequality())
						{
							return Expression.Lambda(Expression.GreaterThanOrEqual(propertyName, typeProperty), type, typeProperty);
						}
						break;
					case QueryFilterOperator.LessThan:
						if (propertyInfo.PropertyType.IsInequality())
						{
							return Expression.Lambda(Expression.LessThan(propertyName, typeProperty), type, typeProperty);
						}
						break;
					case QueryFilterOperator.LessThanOrEqualTo:
						if (propertyInfo.PropertyType.IsInequality())
						{
							return Expression.Lambda(Expression.LessThanOrEqual(propertyName, typeProperty), type, typeProperty);
						}
						break;
					default:
						return default(Expression);
				}

				return default(Expression);
			}
			catch (InvalidOperationException)
			{
				return default(Expression);
			}
		}

		#region Filter Key Creators

		public static string CreateCacheKey(Type type, ParameterExpression parameterExpression)
		{
			return string.Format("{0}.{1}", type.FullName, parameterExpression.Name);
		}

		public static string CreateFilterKey(Type type, PropertyInfo propertyInfo, QueryFilterOperator queryFilterOperator)
		{
			return CreateFilterKey(type, propertyInfo.Name, queryFilterOperator);
		}

		public static string CreateFilterKey(Type type, string property, QueryFilterOperator queryFilterOperator)
		{
			return CreateFilterKey(type.Name, property, queryFilterOperator);
		}

		public static string CreateFilterKey(string type, string property, QueryFilterOperator queryFilterOperator)
		{
			return string.Format("{0}.{1}.{2}", type, property, queryFilterOperator);
		}

		#endregion

		#endregion

		#region Nested type: ParameterExpressionCache

		private struct ParameterExpressionCache
		{
			public ParameterExpression ParameterExpression { get; set; }

			public Type Type { get; set; }
		}

		#endregion
	}
}